﻿using System;
using System.Collections.Generic;
using System.Threading;
using FreeSwitch.EventSocket.Parser;

namespace FreeSwitch.EventSocket.Commands
{
	[Component(ComponentFlags.Singleton)]
	public class CommandDispatcher
	{
		private const int SendTimeout = 50000;
		private readonly IEventChannel _channel;
		private readonly Dictionary<string, MessageHandler> _handlers = new Dictionary<string, MessageHandler>();
		private readonly ILogger _logger = LogManager.GetLogger(typeof (CommandDispatcher));
		private readonly Queue<AsyncCommand> _queue = new Queue<AsyncCommand>();
		private readonly Timer _timeoutTimer;
		private AsyncCommand _current;
		private State _state;
		private AsyncCommand _subscribr;

		public CommandDispatcher(IEventChannel channel)
		{
			_channel = channel;
			_channel.Disconnected += OnDisconnect;
			_handlers.Add("command/reply", HandleCommandReply);
			_handlers.Add("api/response", HandleCommandReply);
			_timeoutTimer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite);
		}

		private void OnDisconnect(object sender, EventArgs e)
		{
			_state = State.Authenticate;
			_timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite);
			_current = null;
			_subscribr = null;
		}

		private void OnTimeout(object state)
		{
			_logger.Warning("Current command expired: " + _current.Command);
			lock (_queue)
			{
				_current = null;
				_timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite);
			}
		}


		private void SendNextCommand()
		{
			lock (_queue)
			{
				_current = null;
				if (_state == State.SendCommands)
				{
					if (_queue.Count == 0)
						return;

					AsyncCommand cmd = _queue.Dequeue();
					SendCommand(cmd);
					return;
				}

				if (_subscribr == null)
					return;

				AsyncCommand tmp = _subscribr;
				_subscribr = null;
				SendCommand(tmp);
			}
		}

		private void SendCommand(AsyncCommand cmd)
		{
			_logger.Debug("Sending command: " + cmd);
			_current = cmd;
			_channel.Send(cmd.Command.ToCommandString() + "\n\n");
			_timeoutTimer.Change(SendTimeout, Timeout.Infinite);
		}


		public void Process(ParsedMessage message)
		{
			string contentType = message.Headers["Content-Type"];
			MessageHandler handler;

			lock (_handlers)
			{
				if (!_handlers.TryGetValue(contentType, out handler))
				{
					_logger.Warning("Failed to find handler for " + contentType);
					return;
				}
			}

			handler(message);
		}

		private void HandleCommandReply(ParsedMessage message)
		{
			_timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite);
			_logger.Debug("Got a reply: " + message);

			if (_current == null)
			{
				_logger.Error("Got a reply without a command: " + message);
				return;
			}

			AsyncCommand cmd = _current;
			cmd.IsCompleted = true;
			cmd.Reply = cmd.Command.ParseReply(message);

			if (cmd.Command is Authenticate)
			{
				if (!cmd.Reply.Success)
					_logger.Error("Failed to authenticate");
				else
					_state = State.Subscribe;
			}
			else if (cmd.Command is Subscribe)
			{
				if (!cmd.Reply.Success)
					_logger.Error("Failed to subscribe.");
				else
					_state = State.SendCommands;
			}

			SendNextCommand();

			cmd.Event.Set();
			if (cmd.Callback != null)
				cmd.Callback(cmd);
		}


		public IAsyncResult BeginSend(ICommand command, AsyncCallback callback, object state)
		{
			var asyncCommand = new AsyncCommand {Callback = callback, Command = command, AsyncState = state};

			if (command is Authenticate)
			{
				SendCommand(asyncCommand);
				return asyncCommand;
			}
			if (command is Subscribe)
			{
				if (_state == State.Subscribe)
					SendCommand(asyncCommand);
				else
					_subscribr = asyncCommand;
				return asyncCommand;
			}

			_queue.Enqueue(asyncCommand);
			if (_current == null)
				ThreadPool.QueueUserWorkItem((s) => SendNextCommand());


			return asyncCommand;
		}

		public ICommandReply EndSend(IAsyncResult result)
		{
			var command = result as AsyncCommand;
			if (command == null)
				throw new InvalidOperationException("Async result is not for command dispatcher");

			if (command.IsCompleted)
				return command.Reply;

			command.Event.WaitOne(Timeout.Infinite);
			return command.Reply;
		}

		#region Nested type: State

		private enum State
		{
			Authenticate,
			Subscribe,
			SendCommands
		} ;

		#endregion
	}

	public delegate void MessageHandler(ParsedMessage message);
}